PythonのAWSライブラリBoto3のSessionはスレッドセーフではないよという話
こんにちは。サービスグループの武田です。
AWSのリソースを操作する方法としてマネジメントコンソールやAWS CLI、そして各プログラミング言語に用意されているSDKが利用できます。人気の高いPythonでは、Boto3というライブラリが提供されています。
さてそのBoto3ですが、基本的な使い方は次のような流れになります。
import boto3 session = boto3.Session() client = session.client("sts") r = client.get_caller_identity() print(r)
- セッション作成
- 各サービス用のクライアント作成
- API呼び出し
通常であれば、これ以上言及することは特にないのですが、マルチスレッドと組み合わせる際に少し注意が必要です。一連の流れで作成しているSession
オブジェクト(および未登場ですがResource
オブジェクト)は スレッドセーフではありません 。これはドキュメントにも明記されています。
試しにエラーを発生させてみましょう。次のようなプログラムを用意してみます。
import concurrent.futures import boto3 def task(session): client = session.client("sts") client.get_caller_identity() session = boto3.Session() with concurrent.futures.ThreadPoolExecutor() as executor: a = executor.submit(task, session) b = executor.submit(task, session) a.result() b.result()
実行すると次のようなエラーになります。
Traceback (most recent call last): File "/python_boto3_multithread/main.py", line 16, in <module> b.result() File "/usr/local/Cellar/[email protected]/3.9.1_5/Frameworks/Python.framework/Versions/3.9/lib/python3.9/concurrent/futures/_base.py", line 433, in result return self.__get_result() File "/usr/local/Cellar/[email protected]/3.9.1_5/Frameworks/Python.framework/Versions/3.9/lib/python3.9/concurrent/futures/_base.py", line 389, in __get_result raise self._exception File "/usr/local/Cellar/[email protected]/3.9.1_5/Frameworks/Python.framework/Versions/3.9/lib/python3.9/concurrent/futures/thread.py", line 52, in run result = self.fn(*self.args, **self.kwargs) File "/python_boto3_multithread/main.py", line 6, in task client = session.client("sts") File "/python_boto3_multithread/.venv/lib/python3.9/site-packages/boto3/session.py", line 258, in client return self._session.create_client( File "/python_boto3_multithread/.venv/lib/python3.9/site-packages/botocore/session.py", line 839, in create_client credentials = self.get_credentials() File "/python_boto3_multithread/.venv/lib/python3.9/site-packages/botocore/session.py", line 441, in get_credentials self._credentials = self._components.get_component( File "/python_boto3_multithread/.venv/lib/python3.9/site-packages/botocore/session.py", line 941, in get_component del self._deferred[name] KeyError: 'credential_provider'
問題の回避策
この問題を解決する一番簡単な方法は、ドキュメントにも書かれているように各スレッドでセッションを生成することです。
@@ -2,15 +2,15 @@ import boto3 -def task(session): +def task(): + session = boto3.Session() client = session.client("sts") client.get_caller_identity() -session = boto3.Session() with concurrent.futures.ThreadPoolExecutor() as executor: - a = executor.submit(task, session) - b = executor.submit(task, session) + a = executor.submit(task) + b = executor.submit(task) a.result() b.result()
これであればエラーは発生しません。
次のdeepcopyを使用した方法は、Python 3.10.9、boto3 1.26.51で dictionary changed size during iteration のエラーが出ることがあり、正常に動作しないことがあるようです。素直にスレッドごとにセッションを作成するのがお勧めです。
一方で、使用するSession
オブジェクトが一時クレデンシャルを利用する等複雑な構築をしている場合などはどうでしょうか。そのようなケースではSession
オブジェクトをコピーすることでエラーを回避できました。プログラムは次のようになります。
@@ -1,5 +1,6 @@ import concurrent.futures import boto3 +import copy def task(session): @@ -9,8 +10,8 @@ session = boto3.Session() with concurrent.futures.ThreadPoolExecutor() as executor: - a = executor.submit(task, session) - b = executor.submit(task, session) + a = executor.submit(task, copy.deepcopy(session)) + b = executor.submit(task, copy.deepcopy(session)) a.result() b.result()
copy.deepcopy()
がミソで、copy.copy()
(シャローコピー)だとダメでした。
まとめ
マルチスレッドプログラミングはスレッドセーフ性や排他制御、競合状態など特有の問題に対処しなければいけませんが、コストに対するリターンは大きいです。ぜひみなさんもたくさん落とし穴にはまって、いいシステムを作っていってください。